﻿using S7.Net;
using S7.Net.Types;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Net.Sockets;

namespace S7NetManager
{
    public class PlcManager:IObserver<int>
    {
        private ConcurrentQueue<DataItemWrapper> _readDataItemQueue = new ConcurrentQueue<DataItemWrapper>();
        private ConcurrentQueue<DataItemWrapper> _writeDataItemQueue = new ConcurrentQueue<DataItemWrapper>();
        private ConcurrentQueue<ClassWrapper> _readClassQueue = new ConcurrentQueue<ClassWrapper>();
        private ConcurrentQueue<ClassWrapper> _writeClassQueue = new ConcurrentQueue<ClassWrapper>();
        private Plc _plc;
        public const int MAX_READ_QUEUE_Capacity = 200;
        public const int MAX_Write_QUEUE_Capacity = 200;
        /// <summary>
        /// dataitem最大的变量数量
        /// </summary>
        public const int MAX_DATA_ITEM_COUNT = 18; 

        private class ClassWrapper
        {
            public ClassWrapper(object source,int db,int startAddr,Action<object> callback)
            {
                this.Source = source;
                this.Db = db;
                this.StartAddr = startAddr;
                this.Callback = callback;
            }

            public Action<object> Callback { get; }

            public object Source { get; }

            public int Db { get; }

            public int StartAddr { get; }

            public override bool Equals(object obj)
            {
                var other = obj as ClassWrapper;
                if (other == null) return false;
                return this.Db == other.Db && this.StartAddr == other.StartAddr;
            }

            public override int GetHashCode()
            {
                return this.Db.GetHashCode() + this.StartAddr.GetHashCode();
            }
        }

        private class DataItemWrapper
        {
            public DataItemWrapper(DataItem source,   Action<DataItem> callback,Action<Exception> errorHandle)
            {
                this.Source = source;              
                this.Callback = callback;
                this.ErrorHandle = errorHandle;
            }

            public Action<DataItem> Callback { get; }

            public Action<Exception> ErrorHandle { get; }

            public DataItem Source { get; }

            public override bool Equals(object obj)
            {
                var other = obj as DataItemWrapper;
                if (other == null) return false;
                if (this.Source.DB != other.Source.DB) return false;
                if (this.Source.BitAdr != other.Source.BitAdr) return false;
                if (this.Source.StartByteAdr != other.Source.StartByteAdr) return false;
                if (this.Source.Count != other.Source.Count) return false;
                if (this.Source.DataType != other.Source.DataType) return false;
                if (this.Source.VarType != other.Source.VarType) return false;
                return true; 
            }

            public override int GetHashCode()
            {
                return this.Source.GetHashCode();
            }
        }

        public PlcManager(IObservable<int> obs)
        {
            if (obs == null)
                throw new ArgumentNullException("Observable不许为空");
            obs.Subscribe(this);
        }

        public bool IsAvailable { get; private set; }

        public void Open(string ip)
        {
            if (_plc != null && _plc.IsConnected)
                _plc.Close();
            _plc = new Plc(CpuType.S71200, ip, 0, 0);

            if (!this.IsConnected)
            {
                _plc.Open();
                this.ConnectStateChanged?.Invoke(this, EventArgs.Empty);
            }
        }

        public string IP => this._plc == null ? null : _plc.IP;

        public bool ReconnectEnable { get; set; }

        private void CheckAvailableChange()
        {
            if (this._plc == null) return;
            using (var client = new TcpClient())
            {
                var result = client.BeginConnect(_plc.IP, _plc.Port, null, null);
                var success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromMilliseconds(50));
                if (success)
                    client.EndConnect(result);
                if (this.IsAvailable != success)
                {
                    IsAvailable = success;
            
                }
            }
            if (IsAvailable)
            {
                if (!this.IsConnected && ReconnectEnable  )
                {
                    this._plc.Open();
                    this.ConnectStateChanged?.Invoke(this, EventArgs.Empty);
                }
            }
            else if (this.IsConnected)
                this.Close();
        }

     

        public event EventHandler ConnectStateChanged;

        public void Close()
        {
            if (this._plc != null && _plc.IsConnected)
            {
                this._plc.Close();
                this.ConnectStateChanged?.Invoke(this,EventArgs.Empty);
            }

        }


        public bool IsConnected => _plc == null ? false : _plc.IsConnected;


        public event Action<string> WriteLog;

        public void Read(DataItem dataItem, Action<DataItem> callback = null,Action<Exception> errorHandle=null)
        {
            var item = new DataItemWrapper(dataItem, callback, errorHandle);
            if (this._readDataItemQueue.Contains(item))
                return;
            if (_readDataItemQueue.Count >= MAX_READ_QUEUE_Capacity)
                return;
            _readDataItemQueue.Enqueue(item);
        }

        public void ReadClass<T>(T obj,int db,int startAddress,Action<T> callback)
            where T : class
        {
            var item = new ClassWrapper(obj, db, startAddress, x => callback(x as T));
            if (_readClassQueue.Contains(item))
                return;
            if (_readClassQueue.Count >= MAX_READ_QUEUE_Capacity)
                return;
            _readClassQueue.Enqueue(item);
        }

        public void WriteClass<T>(T obj, int db, int startAddress, Action<T> callback=null)
            where T : class
        {
            var item = new ClassWrapper(obj, db, startAddress, x => callback(x as T));
            if (this._writeClassQueue.Contains(item))
                return;
            if (_writeClassQueue.Count >= MAX_READ_QUEUE_Capacity)
                return;
            _writeClassQueue.Enqueue(item);
        }

        public void Write(DataItem dataItem, Action<DataItem> callback = null,Action<Exception> errorHandle=null)
        {
            var item = new DataItemWrapper(dataItem, callback, errorHandle);
            if (this._writeDataItemQueue.Contains(item))
                return;
            if (_writeDataItemQueue.Count >= MAX_READ_QUEUE_Capacity)
                return;
            _writeDataItemQueue.Enqueue(item);
        }
 
        void IObserver<int>.OnNext(int value)
        {
            try
            {                
                CheckAvailableChange();
                if (_plc != null && IsAvailable && IsConnected)
                {
                    if (_readDataItemQueue.Count > 0) //检查读DataItem队列
                    {
                        var lst = new List<DataItemWrapper>();
                        var rc = _readDataItemQueue.Count;
                        for (int i = 0; i < rc; i++)
                        {
                            DataItemWrapper item = null;
                            if (_readDataItemQueue.TryDequeue(out item))
                                lst.Add(item);
                        }
                        while (lst.Count > 0)
                        {
                            var subLst =lst.Take(MAX_DATA_ITEM_COUNT).ToList(); //每次取最大限度的dataitem
                            lst.RemoveRange(0, subLst.Count);
                            try
                            {                               
                                _plc.ReadMultipleVars(subLst.Select(x => x.Source).ToList());
                                subLst.ForEach(x => x.Callback?.Invoke(x.Source));
                            }
                            catch 
                            {
                                this.NotifyReadError(subLst);
                            }
                        }
                    }

                    if (_readClassQueue.Count > 0) //检查读Class队列
                    {                  
                        var rc = _readClassQueue.Count;
                        for (int i = 0; i < rc; i++)
                        {
                            ClassWrapper item = null;
                            if (_readClassQueue.TryDequeue(out item))
                            {
                                this._plc.ReadClass(item.Source, item.Db, item.StartAddr);
                                item.Callback?.Invoke(item.Source);
                            }
                        }                        
                    }
                    if (_writeDataItemQueue.Count > 0) //检查写DataItem队列
                    {
                        var lst = new List<DataItemWrapper>();
                        var count = _writeDataItemQueue.Count;
                        for (int i = 0; i < count; i++)
                        {
                            DataItemWrapper item = null;
                            if (_writeDataItemQueue.TryDequeue(out item))
                                lst.Add(item);
                        }
                        while (lst.Count > 0)
                        {
                            var subLst = lst.Take(MAX_DATA_ITEM_COUNT).ToList(); //每次取最大限度的dataitem
                            lst.RemoveRange(0, subLst.Count);
                            try
                            {
                                _plc.Write(subLst.Select(x => x.Source).ToArray());
                                subLst.ForEach(x =>
                                {
                                    x.Callback?.Invoke(x.Source);
                                    this.WriteLog?.Invoke(x.Source.Dump());
                                });
                            }
                            catch 
                            {
                                this.NotifyWriteError(subLst);
                            }
                        }
                    }
                    if (_writeClassQueue.Count > 0)
                    {
                        var count = _writeClassQueue.Count;
                        for (int i = 0; i < count; i++)
                        {
                            ClassWrapper item = null;
                            if (_writeClassQueue.TryDequeue(out item))
                            {
                                _plc.WriteClassAsync(item.Source,item.Db,item.StartAddr);
                                item.Callback?.Invoke(item.Source);
                            }
                        }
                    }
                }
            }
            catch(Exception ex)
            {

            }
        }

        private void NotifyReadError(IEnumerable<DataItemWrapper> items)
        {
            foreach(var item in items)
            {
                try
                {                   
                    _plc.ReadMultipleVars(new List<DataItem>(new DataItem[] { item.Source }));
                }
                catch(Exception ex)
                {
                    item.ErrorHandle?.Invoke(ex);
                }
            }
        }

        private void NotifyWriteError(IEnumerable<DataItemWrapper> items)
        {
            foreach (var item in items)
            {
                try
                {

                    _plc.Write( new DataItem[] { item.Source } );
                }
                catch (Exception ex)
                {
                    item.ErrorHandle?.Invoke(ex);
                }
            }
        }

        void IObserver<int>.OnError(Exception error)
        {
             
        }

        void IObserver<int>.OnCompleted()
        {
           
        }

        ~PlcManager()
        {
            if (this._plc != null && this._plc.IsConnected)
                this._plc.Close();
        }

    }

}
